using System;
using System.Collections.Generic;
using System.Data;
using System.Reflection;
using SqlFu.Internals;
using System.Linq;

namespace SqlFu.DDL.Internals
{
    internal class SchemaFromType
    {
        private readonly Type _tp;
        private TableSchema _schema;
        private readonly TableInfo _ti;

        public SchemaFromType(Type tp)
        {
            _tp = tp;
            _ti = TableInfo.ForType(_tp);
        }

        public string TableName
        {
            get { return _ti.Name; }
        }

        public void Process(TableSchema schema)
        {
            _schema = schema;
            _schema.Name = _ti.Name;
            _schema.CreationOption = _ti.CreationOptions;
            ProcessColumns();
            ProcessIndexes();
            ProcessPrimaryKey();
            ProcessForeignKeys();
        }

        private void ProcessPrimaryKey()
        {
            var att = _tp.GetSingleAttribute<PrimaryKeyAttribute>();
            if (att != null)
            {
                _schema.Constraints.SetPrimaryKey(string.Join(",", att.Columns), att.Name);
                _ti.AutoGenerated = att.AutoIncrement;
            }
            else
            {
                if (!_ti.PrimaryKey.IsNullOrEmpty())
                {
                    _schema.Constraints.SetPrimaryKey(_ti.PrimaryKey);
                }
            }
        }

        private void ProcessIndexes()
        {
#if !NET45
            var att = _tp.GetCustomAttributes<IndexAttribute>();
#else
            var att = _tp.GetCustomAttributes<IndexAttribute>().ToArray();
#endif
            if (att.Length > 0)
            {
                foreach (var idx in att)
                {
                    _schema.Indexes.AddIndex(string.Join(",", idx.Columns), idx.IsUnique, idx.Name);
                }
            }
        }

        private void ProcessForeignKeys()
        {
            foreach (var pi in _tp.GetProperties())
            {
                var fk = pi.GetSingleAttribute<ForeignKeyAttribute>();
                if (fk != null)
                {
                    _schema.Constraints.AddForeignKey(pi.Name, fk.ParentTable, fk.ParentColumn, fk.OnUpdate, fk.OnDelete,
                                                      fk.KeyName);
                }
            }
        }

        private void ProcessColumns()
        {
            foreach (var pi in _tp.GetProperties())
            {
                var opt = pi.GetSingleAttribute<ColumnOptionsAttribute>();
                if (opt != null && opt.Ignore) continue;


                var col = AddColumn(pi, opt ?? ColumnOptionsAttribute.Default);

                var red = pi.GetCustomAttributes<RedefineForAttribute>();
                foreach (var def in red)
                {
                    col.Redefine(def.Database, def.Definition);
                }
            }
        }

        private ColumnDefinition AddColumn(PropertyInfo pi, ColumnOptionsAttribute attr)
        {
            DbType type;
            var tp = pi.PropertyType;
            var asString = pi.GetSingleAttribute<InsertAsStringAttribute>();
            if (asString != null)
            {
                type = DbType.String;
            }
            else
            {
                if (tp.IsEnum)
                {
                    tp = Enum.GetUnderlyingType(tp);
                }
                type = FromType(tp);
            }


            var col = new ColumnDefinition();
            col.Name = pi.Name;
            col.DefaultValue = attr.DefaultValue;
            col.IsIdentity = (_ti.PrimaryKey == col.Name && _ti.AutoGenerated);
            col.Size = attr.Size;
            col.Type = type;
            if (pi.PropertyType.IsNullable())
            {
                col.IsNullable = true;
            }
            else
            {
                col.IsNullable = attr.IsNullable;
            }
            _schema.Columns.AddColumn(col);
            return col;
        }

        public static DbType FromType(Type type)
        {
            var dict = new Dictionary<Type, DbType>();

            dict.Add(typeof (Int32), DbType.Int32);
            dict.Add(typeof (Int32?), DbType.Int32);
            dict.Add(typeof (Int16), DbType.Int16);
            dict.Add(typeof (Int16?), DbType.Int16);
            dict.Add(typeof (Int64), DbType.Int64);
            dict.Add(typeof (Int64?), DbType.Int64);
            dict.Add(typeof (Single), DbType.Single);
            dict.Add(typeof (Single?), DbType.Single);
            dict.Add(typeof (Byte), DbType.Byte);
            dict.Add(typeof (Byte?), DbType.Byte);
            dict.Add(typeof (string), DbType.String);
            dict.Add(typeof (Guid), DbType.Guid);
            dict.Add(typeof (Guid?), DbType.Guid);
            dict.Add(typeof (byte[]), DbType.Binary);
            dict.Add(typeof (bool), DbType.Boolean);
            dict.Add(typeof (bool?), DbType.Boolean);
            dict.Add(typeof (double), DbType.Double);
            dict.Add(typeof (double?), DbType.Double);
            dict.Add(typeof (DateTime), DbType.DateTime);
            dict.Add(typeof (DateTime?), DbType.DateTime);
            dict.Add(typeof (DateTimeOffset), DbType.DateTimeOffset);
            dict.Add(typeof (DateTimeOffset?), DbType.DateTimeOffset);
            dict.Add(typeof (TimeSpan), DbType.String);
            dict.Add(typeof (TimeSpan?), DbType.String);
            dict.Add(typeof (decimal), DbType.Decimal);
            dict.Add(typeof (decimal?), DbType.Decimal);


            DbType rez;
            if (dict.TryGetValue(type, out rez))
            {
                return rez;
            }
            throw new InvalidOperationException(string.Format("Can't map '{0}' to a DbType", type.Name));
        }
    }
}